package fregat.base.net;


import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;
//import java.net.InetAddress;
import java.util.ResourceBundle;

import fregat.base.util.Logger;

/**
 * Client thread to connect to TCP servers. By default the client starts a
 * receive loop after connected. Received data is forwarded to
 * {@link #received(byte[],PrintStream)}, where further processing is done.
 * {@link #connected()} may be overwritten to change behaviour after connect.
 *
 * @author Alexander Mann <br><b><i>ECO_L O G</i> &nbsp Logistiksysteme GmbH</b>
 * @version 1.0
 */
public abstract class TCPClient extends Thread {
  private String host; // name of server
  private int port; // port of server
  private Socket socket; // socket to server
  protected PrintStream out; // out stream to server
  protected DataInputStream in; // in stream from server
  private boolean shutdown; // shutdown flag
  private int reconnectTimeout = 5000; // reconnect timeout
  private boolean active; // if true, the client is an active client that tries to connect to a server
  private Protocol protocol; // used protocol
  private static ResourceBundle resource = ResourceBundle.getBundle("at.ecolog.base.net.resource");
  private Logger logger;

  public TCPClient(String host, int port) {
    this(host,port,Logger.getDefaultCategory());
  }

  /**
   * Creates a new client thread that tries to connect to the TCP server with
   * the specified hostname on the specified port number.
   *
   * @param host Hostname
   * @param port Port number
   * @param logger Logger instance
   */
  public TCPClient(String host, int port, Logger logger) {
    this.host = host;
    this.port = port;
    this.logger = logger;
    active = true;
  }

  public TCPClient(Socket socket) {
    this(socket,Logger.getDefaultCategory());
  }

  /**
   * Creates a new client using the specified socket. This constructor is
   * used by <code>TCPServer</code> as server side client instance.
   *
   * @param socket Socket where remote client has connected
   * @param logger Logging instance
   */
  public TCPClient(Socket socket, Logger logger) {
    this.logger = logger;
    try {
      setSocket(socket);
    } catch (IOException ex) {      
    }
    active = false;
  }

  /**
   * An active client tries to establish a connection, a passive client is instantiated as a server session client
   * @return True, if active
   */
  public boolean isActive() {
    return active;
  }

  /**
   * Tries to connect to the specified host or start receiving if already
   * connected(server side client).
   */
  public void run() {
    if (active) {
      doConnect();
    } else {
      doReceive();
    }
  }

  /**
   * Starts the connection thread. If connected the thread is suspended until
   * connection is lost or client shutdown.
   */
  public void doConnect() {
    while (!shutdown) {
      while (!isConnected() && !shutdown) {
        if (!connect()) {
          logger
              .debug(getName() + ", " + resource.getString("TCPClient_doConnect_1") +
              " " + reconnectTimeout / 1000 + " [s]");
          synchronized(this) {
            try {
              wait(reconnectTimeout);
            } catch (InterruptedException ex) {
            }
          }
        }
      }
    }
  }

  /**
   * Starts the receive loop that waits for data on the specified socket.
   */
  public void doReceive() {
    try {
      byte[] receivedData;

      do {
        receivedData = getProtocol().receive(in);

        if (receivedData != null) {
          received(receivedData, out);
        }
      } while (!shutdown && receivedData != null);

    } catch (EOFException ex) {
      logger.warn(getName() + ", " + "TCPClient.doReceive, EOF");

    } catch (IOException ex) {
      if (!shutdown) {
        logger.warn(getName() + ", " + "TCPClient.doReceive, IO");
      }
    } catch (Exception ex) {
      logger.error(getName() + ", " + "TCPClient.doReceive",ex);
    }

    if (!shutdown) {
      disconnect();
    }
  }

  /**
   * Connects to server
   *
   * @return True, if success
   */
  protected boolean connect() {
    try {
      setSocket(new Socket(host, port));
      logger.debug(getName() + ", " +
          resource.getString("TCPClient_connect_1") +
          " " +
          socket.getInetAddress().getHostAddress() +
          "@" + port);
      connected();
      return true;
    } catch (UnknownHostException ex) {
      logger
          .warn(getName() + ", " + resource.getString("TCPClient_connect_2") + " " + host);
    } catch (IOException ex) {
      logger
          .warn(getName() + ", " + resource.getString("TCPClient_connect_3") + " " + host + "@" + port);
    }
    return false;
  }

  private void setSocket(Socket socket) throws IOException {
    this.socket = socket;
    socket.setTcpNoDelay(true);
    out = new PrintStream(new DataOutputStream(socket.getOutputStream()));
    in = new DataInputStream(socket.getInputStream());
  }

  /**
   * Disconnects from server
   */
  public void disconnect() {
    String address = "?";
    try {
      if (socket != null) {
        address = socket.getInetAddress().toString();
        if(!active) {
          address = address.substring(1);
        }
        socket.close();
        socket = null;
      }
      in.close();
      out.close();
      if(protocol != null) {
        protocol.close();
      }
    } catch (IOException ex) {
      logger.error("TCPClient.disconnect",ex);
    } finally {      
      logger.debug(getName()+", "+resource.getString("TCPClient_disconnect_1").replaceFirst("%1",address));
    }
  }

  /**
   * @return True, if connected
   */
  public boolean isConnected() {
    return socket != null;
  }

  /**
   * Sets reconnect timeout.
   *
   * @param reconnectTimeout Time to wait until connect is retried
   */
  public void setReconnect(int reconnectTimeout) {
    this.reconnectTimeout = reconnectTimeout;
  }

  /**
   * Stops receiving thread and disconnects from arcnet server
   */
  public void shutdown() {
    shutdown = true;
    if (isConnected()) disconnect();
    synchronized(this)  {
      notify();
    }
  }

  /**
   * Sends the given data.
   *
   * @param sendData Data to send
   * @return True, if success
   */
  public synchronized boolean send(String sendData) {
    return send(sendData.getBytes());
  }

  /**
   * Sends the given data.
   *
   * @param sendData Data to send
   * @return True, if success
   */
  public synchronized  boolean send(byte[] sendData) {
    if (isConnected()) {
      return getProtocol().send(out, sendData);
    }
    return false;
  }

  /**
   * Sends the given telegram.
   * @param telegram Data to be sent
   * @return True, fi success
   */
  public synchronized boolean send(Telegram telegram) {
    return send(telegram.getBytes());
  }

  /**
   * Sets the used send and receive protocol.
   *
   * @param protocol
   */
  public void setProtocol(Protocol protocol) {
    this.protocol = protocol;
  }

  /**
   * @return Send and receive protocol (creates <code>DefaultProtocol</code>
   */
  public Protocol getProtocol() {
    if (protocol == null) {
      protocol = new DefaultProtocol(getName());
    }
    return protocol;
  }

  /**
   * This method is called when data was received.
   *
   * @param receivedData
   * @param out
   */
  protected abstract void received(byte[] receivedData, PrintStream out);

  protected void connected() {
    doReceive();
  }

  public String getClientIP() {
    return socket.getInetAddress().toString().substring(1);
  }

  /**
   * @return local adress@port,remote adress@port
   */
  public String toString() {
    StringBuffer buffer = new StringBuffer();
    if (socket != null) {
      buffer.append(socket.getLocalAddress().toString().substring(1));
      buffer.append('@');
      buffer.append(socket.getLocalPort());
      buffer.append(',');
      buffer.append(getClientIP());
      buffer.append('@');
      buffer.append(socket.getPort());
    }
    return buffer.toString();
  }

  public String getKey() {
    return this.toString();
  }
}
